잘 작성된 객체는 하나의 책임만을 지니고 있다. 그렇기에 객체가 복잡한 일을 수행하기 위해서는 다른 객체와 협업해야 한다. 서로 협업을 위해서는 객체는 다른 객체에 대한 지식이 있어야 한다. 지식은 의존성을 만들어 낸다. 이러한 의존성을 관리하지 못하면 애플리케이션은 엉망이 되고 만다.
의존성 이해하기
class Gear: def __init__(self, chainring, cog, rim, tire): self.chanring = chainring self.cog = cog self.rim = rim self.tire = tire def gear_inches(self): return ratio * Wheel(rim, tire).diameter def ratio(self): return chainring / float(cog) class Wheel: def __init__(self, rim, tire): self.rim = rim self.tire = tire def diameter(self): return rim + (tire * 2) Gear(52, 11, 26, 1.5).gear_inches
의존성의 존재를 알기
의존성이 있는다는 것은 어떻게 파악할 수 있을까?
- Gear는 Wheel이라는 이름의 클래스가 있다는 것을 알고 있다.
- Gear는 Wheel이 diameter라는 메서드를 가지고 있는 것을 알고 있다.
- Gear는 Wheel의 생성을 위해 rim, tire를 인자로 넘겨야 하는 것을 알고 있다.
- Gear는 Wheel 인자의 순서를 알고 있다.
이러한 의존성은 Wheel이 변경됨에 따라 어쩔수 없이 Gear도 수정하게 만든다. 협업을 하기때문에 어느정도의 의존성이 생가는 것은 어쩔 수 없다. 하지만 위에서 이야기한 의존성은 불필요하다. 최소한의 지식만을 알고 그 외에는 모르도록 의존성을 관리해야 한다.
의존성 주입하기
class Gear: def __init__(self, chainring, cog, rim, tire): self.chanring = chainring self.cog = cog self.rim = rim self.tire = tire def gear_inches(self): return ratio * Wheel(rim, tire).diameter def ratio(self): return chainring / float(cog) Gear(52, 11, 26, 1.5).gear_inches
불필요한 의존성을 가진 Gear의 재사용성은 떨어지고, 너무 많은 것을 알고 있으면 덜 유용해진다.
class Gear: def __init__(self, chainring, cog, wheel): self.chanring = chainring self.cog = cog self.wheel = wheel def gear_inches(self): return ratio * wheel.diameter def ratio(self): return chainring / float(cog) Gear(52, 11, Wheel(26, 1.5)).gear_inches
의존성 주입을 통해 Gear와 Wheel의 결합을 끊었다. Gear는 diameter를 구현하고 있는 어떠한 객체와도 협업할 수 있게 되었다.
의존성 격리시키기
불필요한 의존성을 모두 제거하면 좋겠지만, 아쉽게도 기술적으론 가능할 지 몰라도 현실에서는 어렵다. 불필요한 의존성을 제거할 수 없는 경우라면 적어도 클래스 안에 격리시켜야 한다.
인스턴스 생성을 격리시키기
class Gear: def __init__(self, chainring, cog, rim, tire): self.chanring = chainring self.cog = cog self.wheel = Wheel(rim, tire) def gear_inches(self): return ratio * wheel.diameter Gear(52, 11, 26, 1.5).gear_inches
class Gear: def __init__(self, chainring, cog, rim, tire): self.chanring = chainring self.cog = cog self.rim = rim self.tire = tire def gear_inches(self): return ratio * wheel.diameter def wheel(self): return Wheel(self.rim, self.tire) Gear(52, 11, 26, 1.5).gear_inches
여전히 Gear에서 Wheel 인스턴스를 만들어야 하고 의존성 또한 높다. 하지만 gear_inches가 가지고 있던 의존성의 일부를 줄였고, Gear가 Wheel에 의존하고 있다는 사실을 명시적으로 드러낼 수 있었다. 이러한 변화는 재사용과 리팩터링을 수월하게 해준다.
외부로 전송되는 메시지를 격리하기
def gear_inches(self): return ratio * wheel.diameter
내부와 외부에 존재하는 의존성은 gear_inches 함수를 취약하게 만든다. 무엇을 수정하든 망가져버릴 위험이 높은 함수이다.
def gear_inches(self): foo = some_result * diameter ... def diameter(self): return wheel.diameter
Wheel 클래스에서 diameter를 바꾼다고 한다면, 그 파급은 이제 Gear 클래스의 작은 함수 단위인 diameter에 한정되어 영향을 미칠 것이다.
초기화 인자로 해시를 이용하기
class Gear: def __init__(self, **kwargs): self.chainring = kwargs.get('chainring', 40) # 기본값 40 self.cog = kwargs.get('cog', 16) # 기본값 16 Gear(chainring=52, cog=15)
class Gear: def __init__(self, *, chainring, cog): self.chanring = chainring self.cog = cog Gear(chainring=52, cog=15)
멀티파라미터 초기화를 고립시키기
# 외부 프레임워크 모듈 # FrameworkGearModule은 건드릴 수 없는 외부 프레임워크이다. # init에는 순서가 고정된 인자들을 필요로 한다. class FrameworkGearModule: def __init__(self, chainring, cog, wheel): self.chainring = chainring self.cog = cog self.wheel = wheel # 팩토리 모듈을 생성해 외부 인터페이스를 감싸서 변화를 받아들일 수 있도록 만든다 class GearWrapper: @classmethod def gear(cls, args): return FrameworkGearModule(args['chainring'], args['cog'], args['wheel']) # 이제 키워드인자로 Gear 인스턴스를 생성할 수 있다. GearWrapper.gear(chainring=52, cog=11, wheel=Wheel(26, 1.5))
외부에 대한 의존성을 메서드로 감싸고 고립시키는 방법을 통해서 우리가 작성한 코드에 의존하게 만든다.
의존성 방향 바꾸기
class Gear: def __init__(self, chainring, cog): self.chainring = chainring self.cog = cog def gear_inches(self, diameter): return ratio * diameter class Wheel: def __init__(self, rim, tire, chainring, cog): self.rim = rim self.tire = tire self.gear = Gear(chainring, cog) def diameter(self): return rim + (tire * 2) def gear_inches(self): return gear.gear_inches(diameter) Wheel(26, 1.5, 52, 11).gear_inches
의존성의 방향을 바꾼다고 해서 지금 당장 크게 달라질 것은 없다. 영원히 변하지 않을 애플리케이션이라면 말이다.
의존성의 방향 결정하기
의존성 방향의 결정에 따라 유지보수가 쉬운 소프트웨어가 될 수 있고, 아니라면 점점 더 수정하기 어려운 소프트웨어가 될 수 있다.
의존성의 방향을 판단하는 세가지 진실이 있다.
- 어떤 클래스는 다른 클래스에 비해 요구사항이 더 자주 바뀐다.
- 구체클래스는 추상클래스보다 수정해야하는 경우가 빈번히 발생한다.
- 의존성이 높은 클래스를 변경하는 것은 코드의 여러 곳에 영향을 미친다.
자기 자신보다 덜 변하는 코드에 의존하라고 말하고 있다.
변경될 가능성이 얼마나 높은지 이해하기
클래스는 항상 변경될 가능성이 있다. 애플리케이션에서 사용하는 모든 클래스는 ‘다른 클래스와 비교해 얼마나 변경되지 않는지’를 기준으로 순위를 매겨볼 수 있다.
구체적인 것과 추상적인 것을 인지하기
추상의 개념은 ‘모든 구체적인 것으로부터 분리된’의 의미를 지닌다. 이전에 Gear 클래스가 Wheel 클래스에 의존했던 상황은 구체적인 코드에 의존하고 있던 것이다. 이후 의존성 주입을 통해 wheel 인스턴스를 받도록 수정되면서 추상적인 코드에 의존하게 된 것이다.
요약
의존성 관리의 핵심은 그 방향을 관리하는 것이다. 평온한 유지보수라는 궁극의 목표를 향한 길은 자기 자신보다 덜 변하는 것에 의존하는 클래스들로 덮어있다.
댓글